<?php
/**
 * compareslice.class.php
 * 
 * 任务调度式有序合片类 - 支持分布式
 */
class compareslice
{
    protected $config = array(
            'compare',
    );
    
    /* 上传目录 */
    private $updFilePath = '';
    private $updTmpPath = '';
    
    /* 合片参数 */
    const _SLICE_NUMBERS = 10; // 每次从分片缓存池中取出的分片数量
    const _BLACK_LIST = 5; // 黑名单时间。单位：秒
    const _DEAL_STATUS = 0; // 状态：分片待处理
    const _DONE_STATUS = 1; // 状态：合片完成
    const _DEALING_STATUS = 2; // 状态：合片中
    const _CANCEL_STATUS = 3; // 状态：任务取消
    const _LOCK_NUMS_MAX = 14; // 意外死锁最大次数，相当于约60秒
    
    /* 本机 IP */
    private $_ip_location = '';
    
    /* 预定义模型 */
    private $_task_block_buffer = ''; // 切片缓存池模型

    /* curl 异步调用地址 */
    private $_url_asynch_curl = '';
    
    /**
     * 初始化
     */
    public function init($method)
    {
        /* 过滤可执行方法 */
        $res = array('status' => '0', 'msg' => '', 'data' => array());
        
        if (! method_exists($this, $method)) {
            throw new MgException('method ' . $method . ' not exists', 10001);
        }
        if (! in_array($method, $this->config)) {
            throw new MgException('method not allow to access', 10002);
        }
        
        /* 文件目录 */
        $this->updFilePath = Mg()->config['upload_root_dir'];
        $this->updTmpPath = Mg()->config['upload_tmp_dir'];
        $this->_updSliceSize = Mg()->config['upload_slice_size'];
        
        /* 本机 IP */
        $this->_ip_location = Mg()->config['ip_location'];
        
        /* 预定义模型 */
        $this->_task_block_buffer = Mg()->mongo->collection('task_block_buffer');

        /* curl 异步调用地址 - 转封装为 mp4 */
        $this->_url_asynch_curl = Mg()->config['url_asynch_curl'] . '/?route=video/trans2mp4';

        /* 执行方法 */
        $this->$method();
    }
    
    /**
     * 合片入口
     */
    public function compare()
    {
        /* 设置脚本超时时间 */
        set_time_limit(0);
        
        /* 检查当前是否有任务正在执行，若有，则跳过（防意外死锁，60秒后自动解锁） */
        $pid_old = (int) file_get_contents(UPLOAD_BOOT . 'bin/compare.pid');
        if ($pid_old > 0) {
            Mg()->log->log('和片中跳过........................', 0, 'compare_');
            return false;
        }
        
        /* 循环合片 */
        try {
            /* 任务开始，标记进程为任务执行中 */
            $this->_markPid(1);
            
            /* 接任务 */
            $blocks = $this->_getBlock();
            
            if (! empty($blocks)) {
                /* 处理任务 */
                $this->_compareBlock($blocks);
            } else {
                /* 没有任务，休息一下 */
                Mg()->log->log('当前没有合片任务', 0, 'compare_');
            }
            
            /* 任务处理完成，标记进程为无任务 */
            $this->_markPid(0);
        } catch (\Exception $e) {
            /* 输出错误 log 日志 */
            Mg()->log->log(['desc' => '合片出错', 'msg' => $e->getMessage()], 1, 'compare_error_');

            /* 异常，释放任务锁 */
            $this->_markPid(0);
        }
        exit();
    }
    
    /**
     * 接任务 - 获取分片信息
     */
    private function _getBlock()
    {
        $blocks = $this->_task_block_buffer->find(function($query) {
            $query->where(['status' => self::_DEAL_STATUS, 'lock_to' => $this->_ip_location])->whereLt('mtime', time());
        })->limit(self::_SLICE_NUMBERS)->toArray();
        
        return $blocks;
    }
    
    /**
     * 处理任务 - 合片
     */
    private function _compareBlock($blocks)
    {
        /* 记录任务开始 */
        Mg()->log->log('合片开始', 0, 'compare_');
        
        foreach ($blocks as $key => $value) {
            /* 检测目录是否存在,不存在则创建 */
            $this->mgmkdir($this->updTmpPath);
            
            /* 分片名 */
            $oldname = $this->updTmpPath . $value['order_id'] . '-' . ($value['order_block'] - 1);
            $newname = $this->updTmpPath . $value['order_id'] . '-' . $value['order_block'];
            
            Mg()->log->log(['desc' => '文件目录', 'oldname' => $oldname ,'newname' => $newname], 0, 'compare_');
            
            /* 检测上一分片合片文件是否存在，上传跳片当前分片计入黑名单等待跳片分片 */
            if (! file_exists($oldname) && $value['order_block'] != 1) {
                /* 计入黑名单操作 */
                $blacklist = $this->_task_block_buffer->update(function($query) use($key) { $query->whereId($key)->set('mtime', time() + self::_BLACK_LIST); }) ? 1 : 0;
                unset($value['content']);
                Mg()->log->log(['desc' => '分片上传跳片', '分片信息' => $value, 'blacklistres' => $blacklist], 1, 'compare_error_');
                
                continue;
            }
            
            /* 获取分片内容 */
            $filecontent = $this->transquote($value);
            
            /* 非最后一分片只发生合片行为 */
            if ($value['nums_block'] != $value['order_block']) {
                /* 写入分片信息 */
                $fp = @fopen($oldname, 'ab');
                $fw = @fwrite($fp, $filecontent) ? 1 : 0;
                fclose($fp);
                
                /* 记录合片结果 */
                Mg()->log->log(['desc' => '合片任务进行中', 'appendResult' => $fw, 'slicenum' => $value['order_block']], 0, 'compare_');
                
                if ($fw && file_exists($oldname)) {
                    rename($oldname, $newname);
                    $dealing = $this->_task_block_buffer->update(function($query) use($key) { $query->whereId($key)->set('status', self::_DEALING_STATUS); }) ? 1 : 0;
                    Mg()->log->log(['desc' => '当前分片合片完成', 'appendResult' => $dealing, 'slicenum' => $value['order_block']], 0, 'compare_');
                } else {
                    Mg()->log->log(['desc' => '当前分片合片失败', 'appendResult' => $fw . 'oldfile: '. $oldname, 'slicenum' => $value['nums_block']], 0, 'compare_error_');
                    continue;
                }
            } else {
                /* 最后一分片完成合片，并移动文件至指定目录 */
                /* 初始化 */
                $output_general = $output_video = $output_audio = array();
                
                /* 写入分片信息 */
                $fp = @fopen($oldname, 'ab');
                $fw = @fwrite($fp, $filecontent);
                fclose($fp);
                
                /* 记录合片结果 */
                Mg()->log->log(['desc' => '最后一分片合片', 'compareres' => $fw], 0, 'compare_');
                
                /* 获取任务信息 */
                $task_order = Mg()->mongo->collection('task_order');
                $taskinfo = $task_order->findOne(function($query) use($value) { $query->whereId($value['order_id']); });
                
                /* 任务不存在或已取消 */
                if (empty($taskinfo) || $taskinfo['status'] == 3) {
                    /* 从缓存池中剔除该分片 */
                    $blacklist = $this->_task_block_buffer->update(function($query) use($key) {
                            $query->whereId($key)->set('status', self::_CANCEL_STATUS); }) ? 1 : 0;
                    unset($value['content']);
                    Mg()->log->log(['desc' => '最后一分片合片失败', 'loginfo' => '任务已被删除或取消, 最后一分片跳过合片', 
                            '分片信息' => $value, 'blacklistres' => $blacklist], 1, 'compare_error_');
                    continue;
                }
                
                /* 处理 md5 值 - 仅处理任务表 @暂时关闭真实md5值的计算 */
                if (! $taskinfo['hashcode']) {
//                    $taskinfo['hashcode'] = md5_file($oldname);
                    $taskinfo['hashcode'] = getRandomString('m');
                    $update_task_order = array(
                            '_id' => $value['order_id'],
                            'hashcode' => $taskinfo['hashcode'],
                            'mtime' => time()
                    );
                    $task_order->update(function($query) use($update_task_order) {
                        $query->whereId($update_task_order['_id'])
                                ->set('hashcode', $update_task_order['hashcode'])
                                ->set('mtime', $update_task_order['mtime']); });
                }
                
                /* 处理文件路径 */
                $sub = strripos($taskinfo['title'], '.');
                $subfix = $sub ? substr($taskinfo['title'], $sub) : '';
                $realsubfix = $this->mgcreatepath($taskinfo['hashcode']) . '_' . getMicrotime() . $subfix;
                $realfile = $this->updFilePath . $realsubfix;
                
                /* 移动文件至 hashcode 分级目录 */
                $this->mgmkdir(dirname($realfile));
                Mg()->log->log(['desc' => '生成最终合片文件', 'oldname' => $oldname, 'realfile' => $realfile], 0, 'compare_');
                
                $mvcmd = 'mv '. $oldname . ' ' . $realfile;
                $res = exec($mvcmd);
                
                Mg()->log->log(['desc' => '移动文件', 'mvcmd' => $mvcmd, 'mvres' => $res], 0, 'compare_');
                
                /* 修改 buffer 信息至已完成合片 */
                $comparecomplete = $this->_task_block_buffer->update(function($query) use($value) {
                        $query->where(['order_id' => $value['order_id']])->set('status', self::_DONE_STATUS); }) ? 1 : 0;
                Mg()->log->log(['desc' => '修改合片结果', 'comparecomplete' => $comparecomplete], 0, 'compare_');
                
                /* 设置文件权限为 777 */
                @exec('chmod 777 ' . $realfile);
                
                /* 上传完成，数据入库 */
                $this->_insertDb($value, $realfile, $realsubfix, $taskinfo);
            }
        }
    }
    
    /**
     * 获取分片内容
     */
    private function transquote($value)
    {
        $slicePath = $this->updTmpPath . $value['content'];
        $fp = @fopen($slicePath, 'rb');
        $filecontent = fread($fp, $this->_updSliceSize);
        return $filecontent;
    }
    
    /**
     * 级联创建文件夹
     */
    private function mgmkdir($path)
    {
        if (! file_exists($path)) {
            $this->mgmkdir(dirname($path));
            @exec('mkdir -m 777 ' . $path);
        }
    }
    
    /**
     * 切割 MD5
     */
    private function mgcreatepath($pathstr, $level = 5)
    {
        $split = str_split($pathstr, 2);
        $path = array_slice($split, 0, $level - 1);
        $filename = array_slice($split, $level - 1);
        return implode('/', $path) . '/' . implode('', $filename);
    }
    
    /**
     * 标记任务进程
     * 0：无任务
     * 1~12：有任务正在处理中
     */
    private function _markPid($pid = 0)
    {
        @file_put_contents(UPLOAD_BOOT . 'bin/compare.pid', $pid);
    }
    
    /**
     * 上传完成，数据入库
     * 
     * @param array $block_info 分片信息
     * @param string $realfile 文件在磁盘上的绝对路径位置
     * @param string $realsubfix 文件在磁盘上的相对路径位置
     * @param array $taskinfo 上传任务信息
     */
    private function _insertDb($block_info, $realfile, $realsubfix, $taskinfo)
    {
        /* 查询文件资源中是否已存在相同文件 */
        $attachment_content = Mg()->mongo->collection('attachment_content_temp');
        $attachinfo = $attachment_content->findOne(function($query) use($block_info) { $query->where(['order_id' => $block_info['order_id']]); });
        
        /* 组织新的文件信息并入库 */
        if (! empty($attachinfo)) {
            /* 准备入库数据 */
            $update_attachment = array(
                    'mediatype' => $attachinfo['mediatype'],
                    'mimetype' => $attachinfo['mimetype'],
                    'image_width' => $attachinfo['image_width'],
                    'image_height' => $attachinfo['image_height'],
                    'filepath' => '/' . $realsubfix,
                    'filesize' => $attachinfo['filesize'],
                    'hashcode' => $attachinfo['hashcode'],
                    'order_id' => $attachinfo['order_id'],
                    'extra' => '',
                    'status' => 1,
                    'mtime' => getMicrotime(),
            );
            
            /* 处理 md5 值 */
            if (! $update_attachment['hashcode']) {
                $update_attachment['hashcode'] = $taskinfo['hashcode'];
            }
            
            /* 如果是图片，先转格式为Jpg，再获取图片长宽 */
            if ($attachinfo['mediatype'] == 'image') {
                /* 转换格式为jpg */
                $update_attachment['filepath'] = trans2jpg($update_attachment['filepath']);
                $realfile = $this->updFilePath . $update_attachment['filepath'];
                Mg()->log->log(['desc' => '图片转换结果', 'realfile' => $realfile], 0, 'compare_');

                /* 获取转换后的图片信息 */
                $update_attachment['filesize'] = filesize($realfile);
                $info_image = getimagesize($realfile);
                if (! empty($info_image)) {
                    $update_attachment['image_width'] = $info_image[0];
                    $update_attachment['image_height'] = $info_image[1];
                }
            }
            
            /* 如果是音视频文件，则获取分辨率、时长等信息 */
            if ($attachinfo['mediatype'] == 'audio' || $attachinfo['mediatype'] == 'video' || $attachinfo['mediatype'] == 'application') {
                /* 此处分多条命令去获取文件不同的信息，后续量大的时候，可优化为 Inform 命令写入文件去执行的方式 */
                /* 获取文件格式信息 */
                if ($attachinfo['mediatype'] == 'application') {
                    $options_general = ' --Inform="General;%InternetMediaType%" ';
                    exec("mediainfo " . $options_general . $realfile, $output_general);
                    $file_info_general = explode('/', $output_general[0]);
                    $update_attachment['mimetype'] = $output_general[0];
                    $update_attachment['mediatype'] = $file_info_general[0];
                }
                
                /* 获取文件视频信息 */
                if ($update_attachment['mediatype'] == 'video') {
                    $options_video = ' --Inform="Video;%Width%,%Height%,%Duration%" ';
                    exec("mediainfo " . $options_video . $realfile, $output_video);
                    $file_info_video = explode(',', $output_video[0]);
                    if (! empty($file_info_video)) {
                        $update_attachment['image_width'] = trim(explode('/', $file_info_video[0])[0]);
                        $update_attachment['image_height'] = trim(explode('/', $file_info_video[1])[0]);
                        $update_attachment['extra']['duration'] = round($file_info_video[2] / 1000);
                    }
                    
                    /* 自动抽取封面 */
                    $result_cover = $this->_clipVideoCover($realfile, 1);
                    if ($result_cover['status'] == 0) {
                        $update_attachment['extra']['cover'] = str_replace($this->updFilePath, '/', $result_cover['data']['filepath']);
                    }
                }
                
                /* 获取文件音频信息 */
                if ($update_attachment['mediatype'] == 'audio') {
                    $options_audio = ' --Inform="Audio;%Duration%" ';
                    exec("mediainfo " . $options_audio . $realfile, $output_audio);
                    if (! empty($output_audio)) {
                        $update_attachment['extra']['duration'] = round($output_audio[0] / 1000);
                    }
                }
            }

            /* 格式文件特殊处理 */
            if (strtolower($attachinfo['suffix']) == 'mts' || strtolower($attachinfo['suffix']) == 'avi') {
                /* 设置文件状态及任务状态为转码中 */
                $set_mts = array(
                    'order_id' => $update_attachment['order_id'],
                    'status' => 7
                );
                $attachment_content->update( function ($query) use ($set_mts) {
                    $query->where(['order_id' => $set_mts['order_id']])
                        ->set('status', $set_mts['status']);
                });

                $set_mts_order = array(
                    '_id' => $update_attachment['order_id'],
                    'status' => 7
                );
                Mg()->mongo->collection('task_order')->update(function($query) use($set_mts_order) {
                    $query->whereId($set_mts_order['_id'])
                        ->set('status', $set_mts_order['status']);
                });

                /* 转封装为 mp4 */
                $request_asynch = array(
                    'order_id' => $update_attachment['order_id'],
                    'filepath' => $realfile
                );
                @$this->_asynchCurl($this->_url_asynch_curl, $request_asynch);

                /* 设置更新信息 */
                $update_attachment['status'] = 7;
            }

            /* 更新附件表 */
            $attachment_content->update( function ($query) use ($update_attachment) {
                $query->where(['order_id' => $update_attachment['order_id']])
                    ->set('mediatype', $update_attachment['mediatype'])
                    ->set('mimetype', $update_attachment['mimetype'])
                    ->set('image_width', $update_attachment['image_width'])
                    ->set('image_height', $update_attachment['image_height'])
                    ->set('filepath', $update_attachment['filepath'])
                    ->set('filesize', $update_attachment['filesize'])
                    ->set('hashcode', $update_attachment['hashcode'])
                    ->set('extra', isset($update_attachment['extra']) ? json_encode($update_attachment['extra']) : '')
                    ->set('status', $update_attachment['status'])
                    ->set('mtime', $update_attachment['mtime']);
            });

            /* 更新订单表 */
            $set_order = array(
                '_id' => $update_attachment['order_id'],
                'status' => $update_attachment['status'],
                'mtime' => time()
            );
            Mg()->mongo->collection('task_order')->update(function($query) use($set_order) {
                $query->whereId($set_order['_id'])
                    ->set('status', $set_order['status'])
                    ->set('mtime', $set_order['mtime']);
            });
        }
    }
    
    /**
     * 抽取视频封面
     * 
     * @param string $filepath 视频文件地址
     * @param integer $offset 偏移时间，即从第几秒开始截取。单位：秒
     * @return array
     */
    private function _clipVideoCover($filepath, $offset = 0)
    {
        $res = array('status' => 0, 'error' => null, 'data' => null);
        
        /* 处理需要处理的文件地址 */
        $input = str_replace('//', '/', $filepath);
        
        /* 处理输出路径 */
        $timenow = getMicrotime();
        $filepath_arr = explode('.', $filepath);
        $filepath_arr[count($filepath_arr) - 2] .= '_' . $timenow;
        $filepath_arr[count($filepath_arr) - 1] = 'jpg';
        $filepath_new = implode('.', $filepath_arr);
        $output = str_replace('//', '/', $filepath_new);
        
        /* 处理时间 */
        $time_start = timeFormat($offset);
        
        /* 执行 */
        $exec = "ffmpeg -ss {$time_start} -t 1 -i {$input} -f image2 -vframes 1 {$output} -y && echo success";
        $result = exec($exec);
        if ($result != 'success') {
            $res['status'] = 1;
            $res['error'] = '请检查磁盘读写权限';
        } else {
            $res['data']['filepath'] = $filepath_new;
        }
        
        return $res;
    }

    /**
     * curl 异步调用（需要在被调用方法中设置 ignore_user_abort(true);）
     *
     * @param string $url
     * @param array $request
     * @return bool|string
     */
    private function _asynchCurl($url, $request)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
        curl_setopt($ch, CURLOPT_TIMEOUT, 1); // 设置超时时间为1s

        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
}